const {
  util: {
    ansi,
    Flushable,
  }
} = require('tui-lib')

const EventEmitter = require('events')

const compareArr = (a, b) => {
  if (a.length !== b.length) {
    return false
  }

  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      return false
    }
  }

  return true
}

module.exports = class WebInterfacer extends EventEmitter {
  constructor(canvas) {
    super()

    this.canvas = canvas

    this.input = document.createElement('textarea')
    this.input.style.position = 'fixed'
    this.input.style.pointerEvents = 'none'
    this.input.style.opacity = 0
    this.input.setAttribute('autocorrect', 'off')
    this.input.setAttribute('autocapitalize', 'off')
    document.body.appendChild(this.input)

    this.flushable = new Flushable(this, true)

    this.fontSize = 12
    this.fontFamily = 'monospace'
    this.linePadding = 1

    const dark = false
    this.colorCodeMapping = {
      '0': 'black',
      '1': 'red',
      '2': 'green',
      '3': 'yellow',
      '4': 'blue',
      '5': 'magenta',
      '6': 'cyan',
      '7': 'white',
      fg: dark ? 'white' : 'black',
      bg: dark ? 'black' : 'white'
    }

    this.initFlushable()
    this.initEvents()

    this.whenCursorLastMoved = Date.now()
    this.oldCursorLine = 0
    this.oldCursorCol = 0

    this.cursorBlinkDuration = 600
  }

  initFlushable() {
    this.flushable.resizeScreen(this.getScreenSize())
    this.flushable.write(ansi.clearScreen())
    this.flushable.flush()
  }

  initEvents() {
    this.input.addEventListener('keydown', evt => {
      this.handleKeyDown(evt)
      evt.preventDefault()
    })

    this.canvas.addEventListener('click', evt => {
    })

    this.canvas.addEventListener('touchstart', evt => {
      const { x, y, width, height } = this.canvas.getBoundingClientRect()

      Object.assign(this.input.style, {
        x: x + 'px',
        y: y + 'px',
        width: width + 'px',
        height: height + 'px'
      })

      this.input.focus()

      this.emitMouse(evt.touches[0], 0)
      evt.preventDefault() // Will prevent mousedown from also firing
    })

    this.canvas.addEventListener('mousedown', evt => this.handleClick(evt))
    this.canvas.addEventListener('wheel', evt => this.handleWheel(evt))
    this.canvas.addEventListener('contextmenu', evt => this.handleClick(evt))
  }

  handleKeyDown(evt) {
    const data = []
    const { keyCode } = evt

    if (keyCode >= 37 && keyCode <= 40) {
      // Arow keys -- these folow a special format, specifically ESC[ followed
      // by 1;N if a modifier key is pressed (N specifies which key) and then a
      // letter (ABCD) to specify which arrow key was pressed.
      data.push(0x1b, 0x5b)

      // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
      const { shiftKey: shift, altKey: alt, ctrlKey: ctrl, metaKey: meta } = evt
      if (shift || alt || ctrl || meta) {
        data.push(0x31, 0x3b, 48 + (
          meta && ctrl && alt && shift && 16 ||
          meta && ctrl && alt && 15 ||
          meta && ctrl && shift && 14 ||
          meta && ctrl && 13 ||
          meta && alt && shift && 12 ||
          meta && alt && 11 ||
          meta && shift && 10 ||
          meta && 9 ||
          shift && alt && ctrl && 8 ||
          alt && ctrl && 7 ||
          shift && ctrl && 6 ||
          ctrl && 5 ||
          shift && alt && 4 ||
          alt && 3 ||
          /* shift && */ 2
        ))
      }

      data.push({
        '37': 0x44,
        '38': 0x41,
        '39': 0x43,
        '40': 0x42
      }[keyCode])
    } else if (keyCode === 8) {
      // Backspace -- send as delete instead.
      // Perhaps this ought to be considered a bug in telchars.js?
      data.push(0x7F)
    } else if (keyCode >= 65 && keyCode <= 90 && evt.ctrlKey) {
      // Ctrl A-Z. Send the index of the letter in the alphabet (A = 1).
      data.push(keyCode - 64)
    } else if (keyCode >= 48 && keyCode <= 126) {
      // Keys that represent ASCII characters -- send the actual value intended
      // by the key press, not just the keyCode (which does not distinguish
      // between, for example, 5 and %).
      data.push(evt.key.charCodeAt(0))
    } else {
      // Some other key -- just send it directly through.
      data.push(keyCode)
    }

    const buffer = Buffer.from(data)
    this.emit('inputData', buffer)
  }

  getMousePosition(evt) {
    const { width: charWidth, height: charHeight } = this.getCharSize()
    const { x: canvasX, y: canvasY } = this.canvas.getBoundingClientRect()

    const clickX = evt.clientX - canvasX
    const clickY = evt.clientY - canvasY

    const col = Math.ceil(clickX / charWidth)
    const line = Math.ceil(clickY / charHeight)

    return {col, line}
  }

  emitMouse(evt, actionNum) {
    const { col, line } = this.getMousePosition(evt)
    this.emit('inputData', [
      0x1b, 0x5b, 0x4d,
      32 + actionNum,
      32 + col, 32 + line
    ])
  }

  handleClick(evt) {
    if (evt.button >= 0 && evt.button <= 2) {
      this.emitMouse(evt, evt.button)
      evt.preventDefault()
    }
  }

  handleWheel(evt) {
    if (evt.deltaY > 0) {
      this.emitMouse(evt, 65)
    } else if (evt.deltaY < 0) {
      this.emitMouse(evt, 64)
    }
  }

  getScreenSize() {
    const { cols, lines } = this.canvas.dataset
    return {
      cols, lines,
      width: cols,
      height: lines
    }
  }

  write() {
    // When write is called, just ignore the data. Instead we'll display
    // the Flushable's screen buffer onto our textarea.
    const { lastFrame } = this.flushable
    this.updateCursor(lastFrame.oldCursorRow, lastFrame.oldCursorCol)
    this.displayChars(lastFrame.oldChars)
  }

  updateCursor(line, col) {
    if (this.flushable.lastFrame.oldShowCursor) {
      if (line !== this.oldCursorLine || col !== this.oldCursorCol) {
        this.whenCursorLastMoved = Date.now()
        this.oldCursorLine = line
        this.oldCursorCol = col
      }
    }
  }

  displayChars(chars) {
    this.updateCanvasSize()

    const ctx = this.canvas.getContext('2d')
    ctx.font = this.font
    ctx.textBaseline = 'middle'

    const { lines, cols } = this.getScreenSize()
    const { width: charWidth, height: charHeight } = this.getCharSize()

    for (let line = 0; line < lines; line++) {
      const lineChars = chars.slice(line * cols, (line + 1) * cols)

      const chunks = []
      let curChunk = {text: '', attributes: []}
      for (let col = 0; col < cols; col++) {
        const char = lineChars[col]
        if (!compareArr(char.attributes, curChunk.attributes)) {
          if (curChunk.text.length) {
            chunks.push(curChunk)
          }
          curChunk = {text: '', attributes: char.attributes}
        }
        curChunk.text += char.char
      }
      chunks.push(curChunk)

      let x = 0
      const y = line * charHeight

      const drawChunk = chunk => {
        const width = charWidth * chunk.text.length

        const fgColorCode = chunk.attributes.find(chr => chr >= 30 && chr < 40) || 39
        const bgColorCode = chunk.attributes.find(chr => chr >= 40 && chr < 50) || 49

        let fgColor = this.colorCodeMapping[fgColorCode - 30]
        let bgColor = this.colorCodeMapping[bgColorCode - 40]

        fgColor = fgColor || this.colorCodeMapping.fg
        bgColor = bgColor || this.colorCodeMapping.bg

        const swapFGBG = () => {
          const temp = fgColor
          fgColor = bgColor
          bgColor = temp
        }

        if (chunk.attributes.find(x => x == 7)) {
          swapFGBG()
        }

        fgColor = fgColor || this.colorCodeMapping.fg

        if (chunk.attributes.find(x => x == 2)) {
          ctx.globalAlpha = 0.5
        }

        if (bgColor) {
          ctx.fillStyle = bgColor
          ctx.fillRect(x, y, width, charHeight)
        }

        ctx.fillStyle = fgColor
        ctx.fillText(chunk.text, x, y + charHeight / 2 + 1)
        x += width

        ctx.globalAlpha = 1.0
      }

      let drawCol = 0
      for (const chunk of chunks) {
        const { text } = chunk

        // If this chunk includes the character that is under the cursor, split
        // up the chunk into three segments - the ones before and after the
        // character, and the one under the cursor. Render the ones around the
        // cursor like normal, but make the chunk of the character under the
        // cursor be inverted (or not-inverted if the chunk was initially
        // inverted).
        if (this.shouldRenderCursor() && line === this.oldCursorLine) {
          const cursorCol = this.oldCursorCol
          if (drawCol <= cursorCol && drawCol + text.length > cursorCol) {
            drawChunk(Object.assign({}, chunk, {
              text: text.slice(0, cursorCol - drawCol)
            }))

            drawChunk(Object.assign({}, chunk, {
              text: text[cursorCol - drawCol],
              attributes: (
                chunk.attributes.includes(7)
                ? chunk.attributes.filter(x => x !== 7)
                : chunk.attributes.concat([7]))
            }))

            drawChunk(Object.assign({}, chunk, {
              text: text.slice(cursorCol - drawCol + 1)
            }))

            continue
          }
        }

        drawChunk(chunk)

        drawCol += text.length
      }
    }
  }

  shouldRenderCursor() {
    if (!this.flushable.lastFrame.oldShowCursor) {
      return false
    }

    const timeSinceLastMoved = Date.now() - this.whenCursorLastMoved
    return Math.floor(timeSinceLastMoved / this.cursorBlinkDuration) % 2 === 0
  }

  getCharSize() {
    const ctx = this.canvas.getContext('2d')
    ctx.font = this.font
    const { width } = ctx.measureText('X')
    const height = this.fontSize + this.linePadding * 2
    return {width, height}
  }

  updateCanvasSize() {
    const { width, height } = this.getCharSize()
    const { lines, cols } = this.getScreenSize()
    this.canvas.width = width * cols
    this.canvas.height = height * lines
  }

  get font() {
    return `${this.fontSize}px ${this.fontFamily}`
  }
}
